winbrew_core\fs\archive/
context.rs

1use std::collections::HashMap;
2use std::fs;
3use std::io::ErrorKind;
4use std::marker::PhantomData;
5use std::path::{Path, PathBuf};
6
7use crate::fs::{FsError, Result};
8
9use super::cleanup::ExtractionCleanup;
10use super::limits::ExtractionLimits;
11use super::platform::PlatformAdapter;
12use super::types::{CachedPath, PathInfo};
13
14pub(crate) struct ExtractionContext<P: PlatformAdapter> {
15    cached_paths: HashMap<PathBuf, CachedPath>,
16    cleanup: ExtractionCleanup,
17    limits: ExtractionLimits,
18    current_total_size: u64,
19    current_file_count: usize,
20    platform: PhantomData<P>,
21}
22
23impl<P: PlatformAdapter> ExtractionContext<P> {
24    pub(crate) fn new(limits: ExtractionLimits) -> Self {
25        Self {
26            cached_paths: HashMap::new(),
27            cleanup: ExtractionCleanup::new(),
28            limits,
29            current_total_size: 0,
30            current_file_count: 0,
31            platform: PhantomData,
32        }
33    }
34
35    pub(crate) fn commit(self) {
36        self.cleanup.commit();
37    }
38
39    pub(crate) fn validate_target(&mut self, path: &Path, destination_dir: &Path) -> Result<()> {
40        let mut current = Some(path);
41        let mut is_final_component = true;
42
43        while let Some(candidate) = current {
44            if candidate == destination_dir {
45                break;
46            }
47
48            match self.inspect_cached(candidate)? {
49                CachedPath::Present(info) => {
50                    if info.is_reparse_point {
51                        return Err(FsError::reparse_point(candidate));
52                    }
53
54                    if is_final_component && !info.is_directory && info.hard_link_count > 1 {
55                        return Err(FsError::hardlinked_target(candidate));
56                    }
57                }
58                CachedPath::Missing => {}
59            }
60
61            is_final_component = false;
62            current = candidate.parent();
63        }
64
65        Ok(())
66    }
67
68    pub(crate) fn ensure_directory_tree(&mut self, path: &Path) -> Result<()> {
69        let mut missing_directories = Vec::new();
70        let mut current = Some(path);
71
72        while let Some(candidate) = current {
73            match self.inspect_cached(candidate)? {
74                CachedPath::Present(info) => {
75                    if !info.is_directory {
76                        return Err(FsError::path_not_directory(candidate));
77                    }
78
79                    break;
80                }
81                CachedPath::Missing => {
82                    missing_directories.push(candidate.to_path_buf());
83                    current = candidate.parent();
84                }
85            }
86        }
87
88        if let Some(deepest_missing) = missing_directories.first() {
89            fs::create_dir_all(deepest_missing)
90                .map_err(|err| FsError::create_directory(deepest_missing, err))?;
91
92            for directory in missing_directories.iter().rev() {
93                self.record_directory(directory);
94            }
95        }
96
97        Ok(())
98    }
99
100    pub(crate) fn check_limits(
101        &mut self,
102        path: &Path,
103        entry_size: u64,
104        compressed_size: u64,
105    ) -> Result<()> {
106        let path_depth = path.components().count();
107
108        if path_depth > self.limits.max_path_depth {
109            return Err(FsError::path_too_deep(
110                path,
111                path_depth,
112                self.limits.max_path_depth,
113            ));
114        }
115
116        if entry_size > 0
117            && (compressed_size == 0
118                || entry_size > compressed_size.saturating_mul(self.limits.max_compression_ratio))
119        {
120            return Err(FsError::suspicious_compression_ratio(
121                path,
122                entry_size,
123                compressed_size,
124                self.limits.max_compression_ratio,
125            ));
126        }
127
128        let new_total_size = self
129            .current_total_size
130            .checked_add(entry_size)
131            .filter(|&size| size <= self.limits.max_total_size)
132            .ok_or_else(|| {
133                FsError::quota_exceeded(
134                    self.limits.max_total_size,
135                    self.current_total_size,
136                    entry_size,
137                )
138            })?;
139
140        let new_file_count = self
141            .current_file_count
142            .checked_add(1)
143            .filter(|&count| count <= self.limits.max_file_count)
144            .ok_or_else(|| {
145                FsError::file_count_exceeded(self.limits.max_file_count, self.current_file_count)
146            })?;
147
148        self.current_total_size = new_total_size;
149        self.current_file_count = new_file_count;
150        Ok(())
151    }
152
153    pub(crate) fn record_file(&mut self, path: &Path) {
154        self.cached_paths.insert(
155            path.to_path_buf(),
156            CachedPath::Present(PathInfo {
157                is_directory: false,
158                is_reparse_point: false,
159                hard_link_count: 1,
160            }),
161        );
162        self.cleanup.record_file(path.to_path_buf());
163    }
164
165    fn record_directory(&mut self, path: &Path) {
166        self.cached_paths.insert(
167            path.to_path_buf(),
168            CachedPath::Present(PathInfo {
169                is_directory: true,
170                is_reparse_point: false,
171                hard_link_count: 1,
172            }),
173        );
174        self.cleanup.record_directory(path.to_path_buf());
175    }
176
177    fn inspect_cached(&mut self, path: &Path) -> Result<CachedPath> {
178        if let Some(cached) = self.cached_paths.get(path) {
179            return Ok(*cached);
180        }
181
182        let state = match P::inspect_path(path) {
183            Ok(info) => CachedPath::Present(info),
184            Err(err) if err.kind() == ErrorKind::NotFound => CachedPath::Missing,
185            Err(err) => return Err(FsError::inspect(path, err)),
186        };
187
188        self.cached_paths.insert(path.to_path_buf(), state);
189        Ok(state)
190    }
191}